Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ

Commit b6c4acb7687e9ee7de0d1a334d01d67a5b3fbb0c


Parents : 9005fb7
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-07-12T06:17:04-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-07-12T11:17:04Z

ci(desktop): sign Windows installers and publish production releases to winget (#6217)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Changes
Diff

diff --git a/.github/workflows/create-or-promote-release.yml b/.github/workflows/create-or-promote-release.yml
index 2af583a4ad..bbbef113cb 100644
--- a/.github/workflows/create-or-promote-release.yml
+++ b/.github/workflows/create-or-promote-release.yml
@@ -132,6 +132,18 @@ jobs:
call-promote-workflow:
if: ${{ !inputs.dry_run && inputs.channel != 'internal' }}
needs: determine-tags
+ # promote.yml's token is capped by this job's grants, and a called
+ # workflow requesting more than its caller grants fails at startup β€”
+ # so this must cover promote.yml's declared workflow-level set, plus
+ # actions: write for its publish-workflow dispatch. Scoped to this job
+ # so call-release-workflow doesn't carry it.
+ permissions:
+ contents: write
+ pull-requests: write
+ statuses: write
+ id-token: write
+ attestations: write
+ actions: write
uses: ./.github/workflows/promote.yml
with:
tag_name: ${{ needs.determine-tags.outputs.tag_to_process }}

diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml
index da19153877..8ba75e1f43 100644
--- a/.github/workflows/promote.yml
+++ b/.github/workflows/promote.yml
@@ -143,6 +143,15 @@ jobs:
update-github-release:
runs-on: ubuntu-24.04-arm
needs: [ prepare-build-info, promote-release ]
+ # actions: write is scoped here β€” only this job's publish-workflow
+ # dispatch needs it, and the other jobs must not get it. Job-level
+ # permissions replace the workflow-level block, so the full set this
+ # job uses is listed.
+ permissions:
+ contents: write
+ pull-requests: write
+ statuses: write
+ actions: write
steps:
- name: Checkout code
uses: actions/checkout@v7.0.0
@@ -167,6 +176,19 @@ jobs:
--draft=false \
--prerelease=${{ inputs.channel != 'production' }}
+ # The edit above is made with GITHUB_TOKEN, and GITHUB_TOKEN-caused
+ # events never start workflow runs β€” winget-publish.yml's `released`
+ # trigger will not fire from it. workflow_dispatch is exempt from that
+ # suppression, so dispatch it explicitly. Soft-fail: a winget hiccup
+ # must not block the changelog stamp or Discord notification.
+ - name: Dispatch winget publish
+ if: ${{ inputs.channel == 'production' }}
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ github.token }}
+ TAG: ${{ inputs.final_tag }}
+ run: gh workflow run winget-publish.yml --ref main -f "tag=$TAG"
+
- name: Stamp CHANGELOG.md for release
if: ${{ inputs.channel == 'production' }}
env:

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4ae68689a8..def49311d9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -62,6 +62,20 @@ on:
required: false
APPLE_TEAM_ID:
required: false
+ # Windows Authenticode signing via Azure Trusted Signing (now "Artifact Signing").
+ # All optional: the signing step skips cleanly when absent, like macOS above.
+ AZURE_TENANT_ID:
+ required: false
+ AZURE_CLIENT_ID:
+ required: false
+ AZURE_CLIENT_SECRET:
+ required: false
+ AZURE_TRUSTED_SIGNING_ENDPOINT:
+ required: false
+ AZURE_TRUSTED_SIGNING_ACCOUNT:
+ required: false
+ AZURE_TRUSTED_SIGNING_CERT_PROFILE:
+ required: false
concurrency:
group: ${{ github.workflow }}-${{ inputs.tag_name }}
@@ -269,6 +283,8 @@ jobs:
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
+ # Secrets aren't readable in step `if:` expressions, so surface presence here.
+ SIGN_WINDOWS: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT != '' && 'true' || 'false' }}
steps:
- name: Checkout code
uses: actions/checkout@v7.0.0
@@ -308,6 +324,39 @@ jobs:
${{ contains(runner.os, 'macOS') && ':desktopApp:packageReleaseUberJarForCurrentOS' || '' }}
'-PaboutLibraries.release=true' '-Pdesktop.release=true' --no-daemon
+ # Authenticode-signs the MSI/EXE installers via Azure Trusted Signing
+ # (rebranded "Artifact Signing"). Skips cleanly when the secrets are
+ # absent, mirroring SIGN_MACOS. Required repository secrets:
+ # AZURE_TENANT_ID – Entra tenant ID
+ # AZURE_CLIENT_ID – app registration (service principal) client ID
+ # AZURE_CLIENT_SECRET – client secret for that app registration
+ # AZURE_TRUSTED_SIGNING_ENDPOINT – e.g. https://eus.codesigning.azure.net
+ # AZURE_TRUSTED_SIGNING_ACCOUNT – Trusted Signing account name
+ # AZURE_TRUSTED_SIGNING_CERT_PROFILE – public-trust certificate profile name
+ # The service principal needs the "Trusted Signing Certificate Profile
+ # Signer" role on the account. Presence is gated on the ENDPOINT secret;
+ # a half-configured set fails here loudly rather than shipping unsigned.
+ # Note: only the installers are signed, not the launcher .exe packaged
+ # inside them β€” SmartScreen reputation attaches to the downloaded file.
+ # Sign the inner .exe between createReleaseDistributable and packaging
+ # if that ever becomes necessary.
+ - name: Sign Windows installers (Azure Trusted Signing)
+ if: runner.os == 'Windows' && env.SIGN_WINDOWS == 'true'
+ uses: azure/artifact-signing-action@v2.0.0
+ with:
+ azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
+ endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
+ signing-account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT }}
+ certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERT_PROFILE }}
+ files-folder: desktopApp/build/compose/binaries/main-release
+ files-folder-filter: msi,exe
+ files-folder-recurse: true
+ file-digest: SHA256
+ timestamp-rfc3161: http://timestamp.acs.microsoft.com
+ timestamp-digest: SHA256
+
# jpackage computes the .deb Depends: line from the build host's package names.
# Ubuntu 24.04 records time_t64 transition names (libasound2t64, libpng16-16t64)
# that don't exist on Debian 12 Bookworm / Raspberry Pi OS, making the .deb

diff --git a/.github/workflows/winget-publish.yml b/.github/workflows/winget-publish.yml
new file mode 100644
index 0000000000..c5b31d99d9
--- /dev/null
+++ b/.github/workflows/winget-publish.yml
@@ -0,0 +1,56 @@
+name: Publish to winget
+
+# The `released` type fires when a release is published as a full release OR
+# when an existing pre-release is flipped to a full release β€” the latter is
+# what promote.yml's production promotion does (`gh release edit
+# --prerelease=false` on the already-published release object). It never fires
+# for drafts or pre-releases, so internal/closed/open promotions are ignored.
+#
+# CAVEAT: promote.yml performs that edit with the workflow's own GITHUB_TOKEN,
+# and events caused by GITHUB_TOKEN never start workflow runs β€” so promote.yml
+# also dispatches this workflow explicitly (workflow_dispatch is exempt from
+# that suppression). The release trigger stays for manually-published releases;
+# workflow_dispatch doubles as the manual retry/seed path.
+on:
+ release:
+ types: [released]
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Production release tag to publish (e.g., v2.8.0)'
+ required: true
+ type: string
+
+# The WINGET_TOKEN PAT does all the work against microsoft/winget-pkgs;
+# nothing in this repo is written.
+permissions: {}
+
+jobs:
+ winget:
+ # Belt and braces for release events β€” `released` should already exclude
+ # these. workflow_dispatch has no release payload and passes through.
+ if: ${{ !github.event.release.prerelease && !github.event.release.draft }}
+ runs-on: ubuntu-latest
+ env:
+ # Secrets aren't readable in step `if:` expressions; skip cleanly until
+ # the token is configured.
+ HAS_WINGET_TOKEN: ${{ secrets.WINGET_TOKEN != '' && 'true' || 'false' }}
+ steps:
+ # Prerequisites (both manual, one-time):
+ # 1. Meshtastic.MeshtasticDesktop must already exist in
+ # microsoft/winget-pkgs β€” winget-releaser only updates existing
+ # packages. Submit the first version by hand with `wingetcreate new
+ # <MSI release asset URL>`.
+ # 2. WINGET_TOKEN secret: a *classic* PAT with the public_repo scope
+ # (fine-grained PATs are not supported), owned by an account that
+ # has (or can create) a fork of microsoft/winget-pkgs.
+ # The version is derived from the release tag with the leading `v`
+ # stripped (v2.8.0 -> 2.8.0), matching the MSI's ProductVersion.
+ - name: Submit MSI manifest to microsoft/winget-pkgs
+ if: env.HAS_WINGET_TOKEN == 'true'
+ uses: vedantmgoyal9/winget-releaser@v2
+ with:
+ identifier: Meshtastic.MeshtasticDesktop
+ installers-regex: '\.msi$'
+ release-tag: ${{ inputs.tag || github.event.release.tag_name }}
+ token: ${{ secrets.WINGET_TOKEN }}

Served by rngit 1.5.4 - Generated in 0.05s